跳到主要内容

SpringBoot Cache 的使用

参考资料 官方教程 Caching Data with Spring 这里主要参考官方的这个教程 参考资料 史上最全的Spring Boot Cache使用与整合

什么是 Spring Cache

Spring Cache 本身是一个缓存体系的抽象实现,并没有具体的缓存能力,要使用 Spring Cache 还需要具体的缓存实现来完成。

Spring Boot 集成了多种 cache 的实现,如果你没有在配置类中声明 CacheManager,那么 SpringBoot 会按顺序在下面的实现类中寻找:

  1. Generic
  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  3. EhCache 2.x
  4. Hazelcast
  5. Infinispan
  6. Couchbase
  7. Redis
  8. Caffeine
  9. Simple

不过注意,如果使用的是 Redis 不需要使用这个 spring-boot-starter-cache 它被内置到 spring-boot-starter-data-redis 这个包里面去了

如果什么都没有配置默认使用的是 Simple 即,使用 ConcurrentHashMap 来作为存储缓存

官方查询图书的案例

配置环境

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

创建实体

这里直接使用 Lombok 省的写 Getter 和 Setter

@Data
@AllArgsConstructor
public class Book {
private String isbn;
private String title;
}

创建 Repository

这个案例直接使用一些延迟来模拟数据库了

public interface BookRepository {
Book getByIsbn(String isbn);
}

实现这个接口(可以看到在每次取得数据时,这里加入个 Sleep 来模拟延迟)

package com.example.caching;

import org.springframework.stereotype.Component;

@Component
public class SimpleBookRepository implements BookRepository {

@Override
public Book getByIsbn(String isbn) {
simulateSlowService();
return new Book(isbn, "Some book");
}

// Don't do this at home
private void simulateSlowService() {
try {
long time = 3000L;
Thread.sleep(time);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}

}

直接使用 Repository

package com.example.caching;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CachingApplication {

public static void main(String[] args) {
SpringApplication.run(CachingApplication.class, args);
}

}

然后调用上面这个 SimpleBookRepository,注意这里继承的这个 CommandLineRunner 接口,它的作用是能在 SpringApplication 启动后执行某些代码

package com.example.caching;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {

private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);

private final BookRepository bookRepository;

public AppRunner(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}

@Override
public void run(String... args) throws Exception {
logger.info(".... Fetching books");
// 注意,这里就第一个和第二个不同,后面的都是一样的
logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
}

}

然后运行后就可以观察到每隔三秒才 “查询” 到一本书

这种时候就可以请出下面的主角,Cache 了

使用 Cache

这时对上面的 SimpleBookRepository 进行改造一下

package com.example.caching;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

@Component
public class SimpleBookRepository implements BookRepository {

@Override
@Cacheable("books")
public Book getByIsbn(String isbn) {
simulateSlowService();
return new Book(isbn, "Some book");
}

// Don't do this at home
private void simulateSlowService() {
try {
long time = 3000L;
Thread.sleep(time);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}

}

然后需要在启动类上加入这个 @EnableCaching 注解

package com.example.caching;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CachingApplication {

public static void main(String[] args) {
SpringApplication.run(CachingApplication.class, args);
}

}

这个 @EnableCaching 注解会激活一个 post-processor 来检查每一个公共方法上是否存在缓存注解,如果找到这样的注释,则会自动创建一个代理来拦截方法调用并相应地处理缓存行为。

这几个缓存注解分别是 @Cacheable@CachePut@CacheEvict

更多细节参考自 官方文档 对这个的说明

然后 Spring Boot 会自动创建一个合适的 CacheManager 来管理缓存(就是上面那些具体的缓存实现),默认什么都没有配置的情况下 Spring Boot 使用的是 ConcurrentHashMap 来作为缓存存储

测试这个用例,可以发现速度显著提升

缓存注解和工具类一览

上面的教程简单介绍了 Spring Cache 怎么用,这里开始详细介绍它包含的一些工具

名称解释
Cache缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
CacheManager缓存管理器,管理各种缓存(cache)组件
@Cacheable主要针对方法配置,能够根据方法的请求参数对其进行缓存
@CacheEvict清空缓存
@CachePut保证方法被调用,又希望结果被缓存。与@Cacheable区别在于是否每次都调用方法,常用于更新
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时key生成策略
serialize缓存数据时value序列化策略
@CacheConfig统一配置本类的缓存注解的属性

注解的主要参数

@Cacheable/@CachePut/@CacheEvict 主要的参数

// 1、value  缓存的名称,在 spring 配置文件中定义,必须指定至少一个
@Cacheable(value="mycache") 或者
@Cacheable(value={"cache1","cache2"}


// 2、key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
@Cacheable(value="testcache",key="#id")


// 3、condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存
@Cacheable(value="testcache",condition="#userName.length()>2")


// 4、unless 否定缓存。当条件结果为TRUE时,就不会缓存。
@Cacheable(value="testcache",unless="#userName.length()>2")


// 5、allEntries(@CacheEvict ) 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
@CacheEvict(value="testcache",allEntries=true)


// 6、beforeInvocation(@CacheEvict) 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存
@CacheEvict(value="testcache",beforeInvocation=true)

SpEL上下文数据

Spring Cache 提供了一些供我们使用的 SpEL 上下文数据,下表直接摘自 Spring 官方文档:

名称位置描述示例
methodNameroot对象当前被调用的方法名#root.methodname
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象实例#root.target
targetClassroot对象当前被调用的目标对象的类#root.targetClass
argsroot对象当前被调用的方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表#root.caches[0].name
Argument Name执行上下文当前被调用的方法的参数,如 findArtisan(Artisan artisan),可以通过 #artsian.id 获得参数#artsian.id
result执行上下文方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict 的 beforeInvocation=false)#result

注意:

1、当我们要使用 root 对象的属性作为 key 时我们也可以将 #root 省略,因为 Spring 默认使用的就是 root 对象的属性。 如

@Cacheable(key = "targetClass + methodName +#p0")

2、使用方法参数时我们可以直接使用 #参数名 或者 #p参数index。 如:

@Cacheable(value="users", key="#id")

@Cacheable(value="users", key="#p0")

SpEL提供的运算符

类型运算符
关系<,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne
算术+,- ,* ,/,%,^
逻辑&&,||,!,and,or,not,between,instanceof
条件?: (ternary),?: (elvis)
正则表达式matches
其他类型?.,?[…]![…],\^[…], $[…]

各个注解详解

缓存 @Cacheable

@Cacheable 注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存。

@Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
public List<NewJob> queryAll(User uid) {
return newJobDao.findAllByUid(uid);
}

此处的 value 是必需的,它指定了你的缓存存放在哪块命名空间。

此处的 key 是使用的 spEL 表达式(就是表示缓存的 Key,不写则按照方法的所有参数进行组合)。

这里有一个小坑,如果把 methodName 换成 method 运行会报错,观察它们的返回类型,原因在于 methodName 是 String 而 method 是 Method。

此处的 User 实体类一定要实现序列化 public class User implements Serializable,否则会报 java.io.NotSerializableException 异常。

打开 @Cacheable 注解的源码,可以看到该注解提供的其他属性,如:

String[] cacheNames() default {}; //和value注解差不多,二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则缓存
String unless() default ""; //条件符合则不缓存
boolean sync() default false; //是否使用异步模式

配置 @CacheConfig

当我们需要缓存的地方越来越多,可以使用 @CacheConfig(cacheNames = {"myCache"}) 注解来统一指定 value 的值,这时可省略 value,如果你在你的方法依旧写上了 value,那么依然以方法的 value 值为准。

如下所示:

@CacheConfig(cacheNames = {"myCache"})
public class BotRelationServiceImpl implements BotRelationService {
@Override
@Cacheable(key = "targetClass + methodName +#p0")//此处没写value
public List<BotRelation> findAllLimit(int num) {
return botRelationRepository.findAllLimit(num);
}
.....
}

查看它的其它属性

String keyGenerator() default "";  //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器

更新 @CachePut

@CachePut 注解的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。简单来说就是既调用方法,又更新缓存数据。修改了数据库的某个数据,同时更新缓存。这个注解是先调用目标方法,然后将结果缓存起来。

CachePut 既然每次都会执行,那还有缓存的意义吗?

只有当结合 @Cacheable 才有用

  • @CachePut 负责更新缓存
  • @Cacheable 负责查询缓存

但需要注意的是该注解的 value 和 key 必须与要更新的缓存相同,也就是与 @Cacheable 相同。示例:

@CachePut(value = "emp", key = "targetClass + #p0")
public NewJob update(NewJob job) {
NewJob newJob = newJobDao.findAllById(job.getId());
newJob.update(job);
return job;
}

@Cacheable(value = "emp", key = "targetClass +#p0")
public NewJob save(NewJob job) {
newJobDao.save(job);
return job;
}

如上所示,它们为同一个方法,如果调用了 update 方法,则会更新缓存,而一般情况下调用 save 则会直接返回缓存

查看它的其它属性:

String[] cacheNames() default {}; //与value二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则缓存
String unless() default ""; //条件符合则不缓存

清除 @CacheEvict

@CacheEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空 。

// allEntries   是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
@CacheEvict(value="testcache",allEntries=true)

// beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存
@CacheEvict(value="testcache",beforeInvocation=true)

使用例:

@Cacheable(value = "emp",key = "#p0.id")
public NewJob save(NewJob job) {
newJobDao.save(job);
return job;
}

//清除一条缓存,key为要清空的数据
@CacheEvict(value="emp",key="#id")
public void delete(int id) {
newJobDao.deleteAllById(id);
}

//方法调用后清空所有缓存
@CacheEvict(value="accountCache",allEntries=true)
public void deleteAll() {
newJobDao.deleteAll();
}

//方法调用前清空所有缓存
@CacheEvict(value="accountCache",beforeInvocation=true)
public void deleteAll() {
newJobDao.deleteAll();
}

其他属性:

String[] cacheNames() default {}; //与value二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则清空

组合 @Caching

组合多个 Cache 注解使用,此时就需要 @Caching 组合多个注解标签了。

@Caching(cacheable = {
@Cacheable(value = "emp",key = "#p0"),
...
},
put = {
@CachePut(value = "emp",key = "#p0"),
...
},evict = {
@CacheEvict(value = "emp",key = "#p0"),
....
})
public User save(User user) {
....
}

直接操作 CacheManager

上文说过这个 Spring Cache 会自动注册一个 CacheManager,实际上可以直接操作这个 CacheManager 来增删改 Cache 的

使用示例

下面这个例子有点乱,它是 SpringSecurity 认证那里使用到的例子,这里直接搬过来了,等以后用到这力再重构笔记

封装一个 CacheName 枚举对象

@AllArgsConstructor
public enum CacheName {

USER("USER"),
PERMISSION("PERMISSION");

private final String cacheName;

public String getCacheName() {
return cacheName;
}
}

上面的这个 getCacheName 是每个枚举对象的方法

创建一个接口,方便操作

public interface Cache {

<T> T get(CacheName cacheName, String key, Class<T> clazz);

void put(CacheName cacheName, String key, Object value);

void remove(CacheName cacheName, String key);
}

编写具体的实现类,可以看到下面的 CacheManager 就是 Spring Boot 自动注入进来的

@Slf4j
@Service("caffeineCache")
public class CaffeineCache implements Cache {
@Autowired
private CacheManager caffeineCacheManager;


@Override
public <T> T get(CacheName cacheName, String key, Class<T> clazz) {
log.debug("{} get -> cacheName [{}], key [{}], class type [{}]", this.getClass().getName(), cacheName, key, clazz.getName());
return Objects.requireNonNull(caffeineCacheManager.getCache(cacheName.getCacheName())).get(key, clazz);
}

@Override
public void put(CacheName cacheName, String key, Object value) {
log.debug("{} put -> cacheName [{}], key [{}], value [{}]", this.getClass().getName(), cacheName, key, value);
Objects.requireNonNull(caffeineCacheManager.getCache(cacheName.getCacheName())).put(key, value);
}

@Override
public void remove(CacheName cacheName, String key) {
log.debug("{} remove -> cacheName [{}], key [{}]", this.getClass().getName(), cacheName, key);
Objects.requireNonNull(caffeineCacheManager.getCache(cacheName.getCacheName())).evict(key);
}
}

然后就能直接操作这个 Cache 了

caffeineCache.remove(CacheName.USER, AuthProvider.getLoginAccount());